package org.apache.lucene.ocean;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.MultiReader;
import org.apache.lucene.ocean.util.Constants;
import org.apache.lucene.ocean.util.SortedListMap;
import org.apache.lucene.ocean.util.Util;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MultiSearcher;
import org.apache.lucene.search.Searchable;
import org.apache.lucene.store.instantiated.InstantiatedIndex;
import org.apache.lucene.store.instantiated.InstantiatedIndexWriter;

/**
 * Keeps a list of OceanMemoryIndex around
 * 
 */
public class WriteableMemoryIndex extends Index {
  private SortedListMap<Long,MemoryIndexSnapshot> snapshotMap = new SortedListMap<Long,MemoryIndexSnapshot>();

  public WriteableMemoryIndex(IndexID id, TransactionSystem system) {
    super(id, system);
  }

  public MemoryIndexSnapshot createIndexSnapshot(Long id) throws IOException {
    return createIndexSnapshot(id, null, null);
  }

  public MemoryIndexSnapshot createIndexSnapshot(Long id, OceanInstantiatedIndexReader[] readers, IndexReader indexReader) throws IOException {
    List<OceanInstantiatedIndexReader> readersList = null;
    if (readers != null) {
      readersList = new ArrayList<OceanInstantiatedIndexReader>(readers.length+1);
      for (OceanInstantiatedIndexReader reader : readers) 
        readersList.add(reader);
    } else
      readersList = new ArrayList<OceanInstantiatedIndexReader>(1);
    OceanInstantiatedIndexReader oceanInstantiatedIndexReader = null;
    if (indexReader == null)
      oceanInstantiatedIndexReader = new OceanInstantiatedIndexReader(new InstantiatedIndex());
    else
      oceanInstantiatedIndexReader = new OceanInstantiatedIndexReader(new InstantiatedIndex(indexReader));
    readersList.add(oceanInstantiatedIndexReader);
    MemoryIndexSnapshot memoryIndexSnapshot = new MemoryIndexSnapshot(id, (OceanInstantiatedIndexReader[])readersList.toArray(new OceanInstantiatedIndexReader[0]));
    snapshotMap.put(id, memoryIndexSnapshot);
    return memoryIndexSnapshot;
  }

  public MemoryIndexSnapshot getIndexSnapshot(Long snapshotId) {
    return snapshotMap.get(snapshotId);
  }

  public void commitNothing(Transaction transaction) throws IndexException, InterruptedException, IOException, Exception {
    MemoryIndexSnapshot latestIndexSnapshot = getLatestIndexSnapshot();
    assert latestIndexSnapshot != null;
    // if
    // (latestIndexSnapshot.getSnapshotId().equals(transaction.getPreviousId()))
    // {
    // }
    transaction.ready(this);
    if (transaction.go()) {
      Long snapshotId = transaction.getId();
      // IndexReader previousIndexReader = latestIndexSnapshot.getIndexReader();
      // MemoryIndexSnapshot(snapshotId, latestIndexSnapshot.indexReaders);
      //MemoryIndexSnapshot newIndexSnapshot = latestIndexSnapshot.newIndexSnapshot(snapshotId);
      OceanInstantiatedIndexReader[] clonedReaders = latestIndexSnapshot.cloneReaders(false);
      createIndexSnapshot(transaction.getId(), clonedReaders);
      removeOldSnapshots(snapshotMap);
    }
  }

  public IndexSnapshotCommitResult commitChanges(Documents documents, Deletes deletes, Analyzer analyzer, Transaction transaction)
      throws InterruptedException, Exception, IOException {
    try {
      if (isClosed()) {
        throw new IOException("index is closed");
      }
      if (isReadOnly() && documents != null && documents.size() > 0) {
        throw new IOException("index not accepting new documents");
      }
      IndexSnapshotCommitResult deletesResult = new IndexSnapshotCommitResult(getId());
      MemoryIndexSnapshot previousIndexSnapshot = getLatestIndexSnapshot();
      
      OceanInstantiatedIndexReader[] clonedReaders = previousIndexSnapshot.cloneReaders(true);
      MultiReader clonedMultiReader = new MultiReader(clonedReaders);
      
      if (deletes != null && deletes.hasDeletes()) {
        deletesResult = applyDeletes(true, deletes, null, clonedMultiReader);
      }
      MemoryIndexSnapshot newIndexSnapshot = null;
      if (documents != null) {
        InstantiatedIndex newIndex = createInstantiatedIndex(documents, analyzer);
        newIndexSnapshot = createIndexSnapshot(transaction.getId(), clonedReaders, new OceanInstantiatedIndexReader(newIndex));
        deletesResult.setNumAdded(documents.size());
        //transaction.addDocsAdded(documents.size());
      } else {
        newIndexSnapshot = createIndexSnapshot(transaction.getId(), clonedReaders, null);
      }
      transaction.ready(this);
      if (transaction.go()) {
        Long snapshotId = transaction.getId();
        //snapshotMap.put(transaction.getId(), newIndexSnapshot);
        removeOldSnapshots(snapshotMap);
        return deletesResult;
      } else {
        snapshotMap.remove(transaction.getId());
        return null;
      }
    } catch (Throwable throwable) {
      transaction.failed(this, throwable);
      if (throwable instanceof Exception) {
        throw (Exception) throwable;
      } else {
        throw new Exception(throwable);
      }
    }
    // return null;
  }
  
  public void createIndexSnapshot(Long id, OceanInstantiatedIndexReader[] readers) {
    
  }
  
  public InstantiatedIndex createInstantiatedIndex(Documents documents, Analyzer analyzer) throws IOException {
    InstantiatedIndex index = new InstantiatedIndex();
    InstantiatedIndexWriter writer = new InstantiatedIndexWriter(index);
    for (Document document : documents) {
      writer.addDocument(document, analyzer);
    }
    writer.commit();
    return index;
  }

  public IndexSnapshotCommitResult commitDeletes(Deletes deletes, Transaction transaction) throws Exception, InterruptedException, IOException {
    return commitChanges(null, deletes, null, transaction);
  }

  public static class IndexSnapshotMultiSearcher extends MultiSearcher implements IndexSnapshotSearchable {
    private IndexSnapshot indexSnapshot;

    public IndexSnapshotMultiSearcher(IndexSnapshot indexSnapshot, Searchable[] searchables) throws IOException {
      super(searchables);
      this.indexSnapshot = indexSnapshot;
    }

    public IndexSnapshot getIndexSnapshot() {
      return indexSnapshot;
    }
  }

  public class MemoryIndexSnapshot extends IndexSnapshot {
    //private List<OceanInstantiatedIndexReader> indexReaders = new ArrayList<OceanInstantiatedIndexReader>();
    private OceanInstantiatedIndexReader[] subReaders;
    private MultiReader indexReader;
    private Integer deletedDoc;
    private Long maxSnapshotId;
    private Long maxDocumentId;
    private Long minSnapshotId;
    private Long minDocumentId;
    private IndexSnapshotMultiSearcher searcher;
    private int maxDoc = 0;

    public MemoryIndexSnapshot(Long snapshotId, OceanInstantiatedIndexReader[] subReaders) throws IOException {
      super(snapshotId);
      this.subReaders = subReaders;
      // indexReaders.iterator();
      /**
      Iterator<OceanInstantiatedIndexReader> iterator = indexReaders.iterator();
      while (iterator.hasNext()) {
        OceanInstantiatedIndexReader reader = iterator.next();
        int maxDoc = reader.maxDoc();
        if (maxDoc == 0) {
          iterator.remove();
        }
        this.maxDoc += maxDoc;
        // throw new RuntimeException("maxDoc: 0");
        // }
      }
      **/
    }
    
    public OceanInstantiatedIndexReader[] cloneReaders(boolean removeMaxDocZero) {
      List<OceanInstantiatedIndexReader> cloneList = new ArrayList<OceanInstantiatedIndexReader>();
      for (OceanInstantiatedIndexReader reader : subReaders) {
        if (removeMaxDocZero) {
          if (reader.maxDoc() >= 0) 
            cloneList.add(reader);
        } else 
          cloneList.add(reader);
      }
      return cloneList.toArray(new OceanInstantiatedIndexReader[0]);
    }

    public IndexSnapshotSearchable getSearcher() {
      if (searcher == null) {
        try {
          if (subReaders.length > 0) {
            Searchable[] searchables = new Searchable[subReaders.length];
            for (int x=0; x < searchables.length; x++) {
              searchables[x] = new IndexSearcher(subReaders[x]);
            }
            searcher = new IndexSnapshotMultiSearcher(this, searchables);
          }
        } catch (IOException ioException) {
          throw new RuntimeException(ioException);
        }
      }
      return searcher;
    }

    public void delete() throws Exception {
      snapshotMap.remove(snapshotId);
    }

    public Long getMinDocumentId() throws IOException {
      if (minDocumentId == null) {
        String string = getMin(Constants.DOCUMENTID);
        if (string == null)
          return null;
        minDocumentId = Util.longFromEncoded(string);
      }
      return minDocumentId;
    }

    public Long getMinSnapshotId() throws IOException {
      if (minSnapshotId == null) {
        String string = getMin(Constants.SNAPSHOTID);
        if (string == null)
          return null;
        minSnapshotId = Util.longFromEncoded(string);
      }
      return minSnapshotId;
    }

    public Long getMaxSnapshotId() throws IOException {
      if (maxSnapshotId == null) {
        String string = getMax(Constants.SNAPSHOTID);
        if (string == null)
          return null;
        maxSnapshotId = Util.longFromEncoded(string);
      }
      return maxSnapshotId;
    }

    public Long getMaxDocumentId() throws IOException {
      if (maxDocumentId == null) {
        String string = getMax(Constants.DOCUMENTID);
        if (string == null)
          return null;
        maxDocumentId = Util.longFromEncoded(string);
      }
      return maxDocumentId;
    }

    //public MemoryIndexSnapshot newIndexSnapshot(Long id) throws IOException {
    //  return newIndexSnapshot(id, (InstantiatedIndex) null);
    //}

    //public MemoryIndexSnapshot newIndexSnapshot(Long id, IndexReader indexReader) throws IOException {
    //  return newIndexSnapshot(id, new InstantiatedIndex(indexReader));
    //}

    //public MemoryIndexSnapshot newIndexSnapshot(Long id, InstantiatedIndex instantiatedIndex) throws IOException {
      //List<OceanInstantiatedIndexReader> newReaders = new ArrayList<OceanInstantiatedIndexReader>(indexReaders.size() + 1);
      //for (OceanInstantiatedIndexReader reader : indexReaders) {
      //  newReaders.add((OceanInstantiatedIndexReader) reader.clone());
      //}
      //if (instantiatedIndex != null)
      //  newReaders.add(new OceanInstantiatedIndexReader(instantiatedIndex));
      //MemoryIndexSnapshot newMemoryIndexSnapshot = new MemoryIndexSnapshot(id);
      //for (OceanInstantiatedIndexReader reader : indexReaders) {
      //  if (reader.maxDoc() > 0) newMemoryIndexSnapshot.add((OceanInstantiatedIndexReader) reader.clone());
      //}
     // newMemoryIndexSnapshot.add(new OceanInstantiatedIndexReader(instantiatedIndex));
     // WriteableMemoryIndex.this.snapshotMap.put(id, newMemoryIndexSnapshot);
     // return newMemoryIndexSnapshot;
    //}

    // void add(IndexReader indexReader) throws IOException {
    // InstantiatedIndex newIndex = new InstantiatedIndex(indexReader);
    // add(newIndex);
    // }

    // private void add(InstantiatedIndex newIndex) {
    // OceanInstantiatedIndexReader newReader = new
    // OceanInstantiatedIndexReader(newIndex);
    // indexReaders.add(newReader);
    // /}

    public int deletedDoc() {
      if (deletedDoc == null) {
        deletedDoc = getIndexReader().maxDoc() - getIndexReader().numDocs();
      }
      return deletedDoc;
    }

    public int maxDoc() {
      return getIndexReader().maxDoc();
    }

    public IndexReader getIndexReader() {
      if (indexReader == null) {
        List<OceanInstantiatedIndexReader> list = new ArrayList<OceanInstantiatedIndexReader>(subReaders.length);
        for (OceanInstantiatedIndexReader reader : subReaders) {
          if (reader.maxDoc() > 0) 
            list.add(reader);
        }
        indexReader = new MultiReader((IndexReader[])list.toArray(new IndexReader[0]));
      }
      return indexReader;
    }
  }

  public MemoryIndexSnapshot getLatestIndexSnapshot() {
    return snapshotMap.lastValue();
  }

  public boolean rollback(Long snapshotId) {
    return snapshotMap.remove(snapshotId) != null;
  }
}
